use super::{data::Query, util, UserState};
use chrono::Utc;
use futures::stream::StreamExt;
use mongodb::{
    bson::{doc, oid::ObjectId, Bson, Document},
    Collection,
};
use serde::{Deserialize, Serialize};
use tide::{convert::json, http::Method, Body, Error, Middleware, Next, Request, Response, StatusCode};

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UserInfo {
    pub id: ObjectId,
    pub role: ObjectId,
}

#[derive(Clone)]
pub struct TokenGenerator {
    /// Secret key to create user token, must be exactly 32 bytes.
    pub secret: Vec<u8>,
    // Time to live in seconds of user token.
    pub ttl: u32,
}

impl TokenGenerator {
    pub fn timestamp(&self) -> u32 {
        use std::time::{SystemTime, UNIX_EPOCH};
        let duration = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("Failed to obtain timestamp from system clock.");
        duration.as_secs() as u32
    }

    pub fn create_token(&self, user: &Document) -> tide::Result<String> {
        let user_info = UserInfo {
            id: user.get_object_id("_id")?.clone(),
            role: ObjectId::with_string(user.get_str("role")?)?,
        };
        let mut payload: Vec<u8> = Vec::with_capacity(24);
        payload.extend_from_slice(&user_info.id.bytes());
        payload.extend_from_slice(&user_info.role.bytes());
        let token = branca::encode(&payload, &self.secret, self.timestamp())?;
        Ok(token)
    }

    pub fn decode_token(&self, token: &str) -> tide::Result<UserInfo> {
        use std::convert::TryInto;
        match branca::decode(token, &self.secret, self.ttl) {
            Ok(payload) => {
                if payload.len() == 24 {
                    let id = ObjectId::with_bytes(payload[0..12].try_into().unwrap());
                    let role = ObjectId::with_bytes(payload[12..].try_into().unwrap());
                    Ok(UserInfo { id, role })
                } else {
                    Err(Error::from_str(StatusCode::Unauthorized, "Invalid token length"))
                }
            }
            Err(err) => match err {
                branca::errors::Error::ExpiredToken => Err(Error::from_str(StatusCode::Unauthorized, "Token expired")),
                _ => Err(Error::from_str(StatusCode::Unauthorized, "Invalid token")),
            },
        }
    }
}

pub struct TokenMiddleware {
    pub public_readable_paths: Vec<String>,
    pub public_writable_paths: Vec<String>,
    pub token_generator: TokenGenerator,
    pub users: Collection,
}

fn can_access(path_prefixes: &Vec<String>, path: &str) -> bool {
    path_prefixes.iter().any(|route| path.starts_with(route))
}

#[tide::utils::async_trait]
impl<State: Clone + Send + Sync + 'static> Middleware<State> for TokenMiddleware {
    async fn handle(&self, mut req: Request<State>, next: Next<'_, State>) -> tide::Result {
        let path = req.url().path();
        if req.method() == Method::Get && can_access(&self.public_readable_paths, path)
            || can_access(&self.public_writable_paths, path)
        {
            Ok(next.run(req).await)
        } else {
            if let Some(token) = req.header("Authorization").map(|value| value.last().as_str()) {
                let user_info = self.token_generator.decode_token(token)?;
                if let Some(user) = self.users.find_one(doc! {"_id": &user_info.id}, None).await? {
                    if user.get_bool("enabled")? {
                        if has_role(user.get_array("roles")?, &user_info.role.to_hex()) {
                            req.set_ext(user_info);
                            Ok(next.run(req).await)
                        } else {
                            Err(Error::from_str(StatusCode::Unauthorized, "Invalid role"))
                        }
                    } else {
                        Err(Error::from_str(StatusCode::Unauthorized, "User account is disabled"))
                    }
                } else {
                    Err(Error::from_str(StatusCode::Unauthorized, "User account not found"))
                }
            } else {
                Err(Error::from_str(StatusCode::Unauthorized, "No Authorization header"))
            }
        }
    }
}

fn has_role(roles: &Vec<Bson>, role: &str) -> bool {
    roles.into_iter().filter_map(|val| val.as_str()).any(|id| id == role)
}

pub async fn register(mut req: Request<UserState>) -> tide::Result<impl Into<Response>> {
    let mut user = util::parse_request_body(&mut req).await?;
    // ensure username is unique
    let username = user.get_str("username")?;
    if req
        .state()
        .users
        .find_one(doc! {"username": username}, None)
        .await?
        .is_some()
    {
        return Err(Error::from_str(StatusCode::BadRequest, "username already exists"));
    }
    // encrypt password
    let password = user.get_str("password")?;
    let hash = create_password_hash(password)?;
    let password = user.get_mut("password").unwrap();
    *password = Bson::String(hash);
    // add timestamp
    let now = Utc::now();
    user.insert("createdAt", now);
    // create user
    let result = req.state().users.insert_one(user, None).await?;
    let response = Response::builder(StatusCode::Created).body(json!({
        "objectId": result.inserted_id.as_object_id().unwrap().to_hex(),
        "createdAt": util::format_datetime(&now)
    }));
    Ok(response)
}

pub async fn login(mut req: Request<UserState>) -> tide::Result<impl Into<Response>> {
    let params = util::parse_request_body(&mut req).await?;
    let username = params.get_str("username")?;
    let password = params.get_str("password")?;
    let role = params.get_str("role").ok();
    let result = req.state().users.find_one(doc! {"username": username}, None).await?;
    if let Some(mut user) = result {
        if bcrypt::verify(password, user.get_str("password")?)? {
            user.remove("password");
            let roles = user.get_array("roles")?;
            let role = role
                .filter(|id| has_role(&roles, id))
                .or(roles.first().and_then(|val| val.as_str()))
                .unwrap_or("")
                .to_string();
            user.insert("role", role);
            let token = req.state().token_generator.create_token(&user)?;
            user.insert("token", token);
            util::make_json_friendly(&mut user)?;
            Ok(Body::from_json(&user)?)
        } else {
            Err(Error::from_str(StatusCode::Unauthorized, "Password error"))
        }
    } else {
        Err(Error::from_str(StatusCode::NotFound, "User not found"))
    }
}

pub async fn switch_role(mut req: Request<UserState>) -> tide::Result<impl Into<Response>> {
    let params = util::parse_request_body(&mut req).await?;
    let role = params.get_str("role")?;
    if let Some(user) = req.ext::<UserInfo>() {
        if let Some(mut user) = req.state().users.find_one(doc! {"_id": &user.id}, None).await? {
            let roles = user.get_array("roles")?;
            if has_role(&roles, role) {
                user.insert("role", role);
                let token = req.state().token_generator.create_token(&user)?;
                return Ok(json!({ "token": token }));
            }
        }
    }
    return Err(Error::from_str(
        StatusCode::Unauthorized,
        "Permission denied in switch role",
    ));
}

pub async fn list(req: Request<UserState>) -> tide::Result<impl Into<Response>> {
    let query: Query = req.query()?;
    let filter = query.create_filter()?;
    let find_options = query.create_find_options();

    let count = if query.count == Some(1) {
        Some(req.state().users.count_documents(filter.clone(), None).await?)
    } else {
        None
    };
    let mut cursor = req.state().users.find(filter, find_options).await?;
    let mut results = Vec::new();
    while let Some(result) = cursor.next().await {
        match result {
            Ok(mut user) => {
                user.remove("password");
                util::make_json_friendly(&mut user)?;
                results.push(user);
            }
            Err(_) => {}
        }
    }
    if let Some(count) = count {
        Ok(json!({ "results": results, "count": count }))
    } else {
        Ok(json!({ "results": results }))
    }
}

pub async fn info(req: Request<UserState>) -> tide::Result<impl Into<Response>> {
    let id = req.param("id")?;
    let filter = doc! {"_id": ObjectId::with_string(id)?};
    let result = req.state().users.find_one(filter, None).await?;
    if let Some(mut user) = result {
        user.remove("password");
        util::make_json_friendly(&mut user)?;
        Ok(Body::from_json(&user)?)
    } else {
        Err(Error::from_str(StatusCode::NotFound, "User not found"))
    }
}

pub async fn update(mut req: Request<UserState>) -> tide::Result<impl Into<Response>> {
    let id = req.param("id")?;
    let filter = doc! {"_id": ObjectId::with_string(id)?};
    let mut user = util::parse_request_body(&mut req).await?;
    // encrypt password
    if let Ok(password) = user.get_str("password") {
        let hash = create_password_hash(password)?;
        let password = user.get_mut("password").unwrap();
        *password = Bson::String(hash);
    }
    let mut update = Document::new();
    update.insert("$set", user);
    update.insert("$currentDate", doc! { "updatedAt": true });

    let result = req.state().users.update_one(filter, update, None).await?;
    Ok(json!(result))
}

pub async fn change_password(mut req: Request<UserState>) -> tide::Result<impl Into<Response>> {
    let id = req.param("id")?;
    let filter = doc! {"_id": ObjectId::with_string(id)?};
    let passwords = util::parse_request_body(&mut req).await?;

    if let Some(user) = req.state().users.find_one(filter.clone(), None).await? {
        // verify old password
        if bcrypt::verify(passwords.get_str("old_password")?, user.get_str("password")?)? {
            // encrypt new password
            let hash = create_password_hash(passwords.get_str("new_password")?)?;

            let mut update = Document::new();
            update.insert("$set", doc! {"password": hash});
            update.insert("$currentDate", doc! { "updatedAt": true });
            let result = req.state().users.update_one(filter, update, None).await?;
            Ok(json!(result))
        } else {
            Err(Error::from_str(StatusCode::Unauthorized, "Password error"))
        }
    } else {
        Err(Error::from_str(StatusCode::BadRequest, "Invalid user id"))
    }
}

pub fn create_password_hash(password: &str) -> bcrypt::BcryptResult<String> {
    bcrypt::hash(password, bcrypt::DEFAULT_COST)
}
